package nl.sogyo.objectvisualisatie.GUI;

import java.awt.Container;
import java.awt.Window;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map.Entry;

import javax.media.opengl.DebugGL;
import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCanvas;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.glu.GLU;
import javax.swing.JComponent;
import javax.swing.SwingUtilities;

import nl.sogyo.objectvisualisatie.core.Model;
import nl.sogyo.objectvisualisatie.data.ObjectInfo;

import com.sun.jdi.Field;
import com.sun.opengl.util.FPSAnimator;
import com.sun.opengl.util.GLUT;


public class JoGLCanvas extends GLCanvas implements GLEventListener, MouseListener, MouseMotionListener{

    private static final long serialVersionUID = 1L; 	// Serial version UID. */    
        
    private FPSAnimator animator;						// The OpenGL animator. */
    private Model model;
    private GL gl = null;
    private GLU glu;									// The GL unit (helper class). */
    private GLUT glut = null;
	
    private int fps = 27;								// The frames per second setting.					//
    private float cameraPosZco = 30;					// camera position Z-as with respect to (0,0) 
    private float floatingPoints = cameraPosZco*(1f-(1f/6f)); //camera position - 16% (height - depth ration)
    
    private float transX, transY, transZ = 0.0f;		//floats for positioning an object
    private String relationType = "relations"; // other types  relations, initializations
    
    private int dragged;				// ObjectId that is currently dragged
    private float oldMouseX;			// Old position of the X-co object
    private float oldMouseY;			// Old position of the y-co object
    private boolean isDragging = false;	// If currently dragging
    
    public JoGLCanvas()
    {
    	addGLEventListener(this);
    	addMouseListener(this);
    	addMouseMotionListener(this);
    }
   
    public void init(GLAutoDrawable drawable) {
        drawable.setGL(new DebugGL(drawable.getGL()));
        gl = drawable.getGL();

        gl.glEnable(GL.GL_DEPTH_TEST);								// Enable z- (depth) buffer for hidden surface removal. 
        gl.glDepthFunc(GL.GL_LEQUAL);								// Enable z- (depth) buffer for hidden surface removal. 
        gl.glShadeModel(GL.GL_SMOOTH);								// Enable smooth shading.
        gl.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);					// Define "clear" bgcolor.
        gl.glHint(GL.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST); // We want a nice perspective.
        
        gl.glEnable(GL.GL_TEXTURE_2D);								// Enable 2D Texture Mapping
        gl.glViewport(0, 0, WIDTH, HEIGHT);
       // gl.glPushAttrib(GL.GL_VIEWPORT_BIT);
        
        
        glu = new GLU(); // Create GLU.

        // Start animator.
        animator = new FPSAnimator(this, fps);
        animator.start();
    }

    /**
     *  
     * @see javax.media.opengl.GLEventListener#display(javax.media.opengl.GLAutoDrawable)
     */
    public void display(GLAutoDrawable drawable) {
        if (!animator.isAnimating()) {
            return;
        }
        gl = drawable.getGL();
        
        glut = new GLUT();
        
        gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT); // Clear screen.
        gl.glColor3f(1.0f, 1.0f, 1.0f);								 // Clear screen.
        
        
        setCamera(gl, glu, cameraPosZco); // Set camera cameraPosZco form point (0,0,0)

        drawObjectModel(gl);
        revalidateGUI();
    }
    
    /**
     * This method is responsible for the creating the object model. It calls
     * all different method which execute a subaspect of the model
     * @param gl
     */
	public void drawObjectModel(GL gl)
    {
    	ArrayList<ObjectInfo> list = (ArrayList<ObjectInfo>) model.getobjectsList().clone();
    	for(int i = 0; i < list.size(); i++){
			ObjectInfo objectinfo = list.get(i);
			//handle and draw relation
			handleRelations(objectinfo);		
			//handle translation form point (0,0,0)
	        placeObjects(objectinfo, list.size(), i);
	        //draw object on the canvas
	        drawObject(objectinfo, objectinfo.getX(),  objectinfo.getY(),  objectinfo.getZ());
	        //drawtext
	        String className =objectinfo.getClassName() + " (" + objectinfo.getId() +")";// objectinfo.getClassName();//
	        createText(className, objectinfo.getX() - 1.0f, objectinfo.getY() + 0.25f, objectinfo.getZ());
	        // handle en draw fields
	        handleFields(objectinfo);
		}
    }
    
    /**
     * Method that draws the class shape
     * @param objectinfo = Object that will be drawn
     * @param transX	 = translation over the X-axes
     * @param transY	 = translation over the Y-axes
     * @param transZ	 = translation over the Z-axes
     */
    public void drawObject(ObjectInfo objectinfo, float transX, float transY, float transZ)
    {
    	gl.glLoadIdentity();	
        gl.glTranslatef(0.0f,0.0f,transZ);
        float classTopHeight = objectinfo.getTopHeight();
        if(objectinfo.isSelected() == true) gl.glColor3f(0.9f, 0.9f, 0.9f);
        else gl.glColor3f(1.0f, 1.0f, 0.8f);
    	float width = objectinfo.getWidth();

    	/*************** classname square *******/	
		gl.glBegin(GL.GL_QUADS);
	        gl.glVertex3f(transX -(width/2),transY , transZ); //Bottom Left
	        gl.glVertex3f(transX +(width/2),transY , transZ); //Bottom Right
	        gl.glVertex3f(transX +(width/2),transY + classTopHeight, transZ); //Top Right
	        gl.glVertex3f(transX -(width/2),transY + classTopHeight, transZ); //Top left
	    gl.glEnd();
	    
	    /*************** Fields square ***********/	    
	    float fieldTopHeight = objectinfo.getHeight();
	    if(fieldTopHeight < classTopHeight){ fieldTopHeight = classTopHeight;}
	    gl.glBegin(GL.GL_QUADS);
	        gl.glVertex3f(transX -(width/2),transY -fieldTopHeight, transZ); //Bottom Left
	        gl.glVertex3f(transX +(width/2),transY -fieldTopHeight, transZ); //Bottom Right
	        gl.glVertex3f(transX +(width/2),transY, transZ); //Top Right
	        gl.glVertex3f(transX -(width/2),transY, transZ); //Top left
        gl.glEnd();
	    
	    /*************** classname lines **********/
        gl.glColor3f(0f, 0f, 0f);
        
		//center left -> center right
		drawLine(transX -(width/2), transY, transZ, //(x1, y1)
				 transX +(width/2), transY, transZ ); //(x2, y2)
		//center left -> top left
		drawLine(transX -(width/2), transY, transZ, 
				 transX -(width/2),transY +classTopHeight, transZ); 
		//center right -> top right
		drawLine(transX +(width/2),transY, transZ, 
				 transX +(width/2),transY +classTopHeight, transZ); 
		//Top left -> top right
		drawLine(transX -(width/2),transY +classTopHeight, transZ, 
				 transX +(width/2),transY +classTopHeight, transZ); 
		
		/*************** Field lines ***************/
		//bottom left -> bottom right
		drawLine(transX -(width/2), transY -fieldTopHeight, transZ, //(x1, y1)
				 transX +(width/2), transY -fieldTopHeight, transZ); //(x2, y2)
		//center left -> bottom left
		drawLine(transX -(width/2), transY -fieldTopHeight, transZ,
				(transX -(width/2)),transY, transZ);
		//center right -> bottom right
		drawLine(transX +(width/2),transY -fieldTopHeight, transZ,
				 transX +(width/2),transY, transZ);
    }
    
    /**
     * Draw a line between to points
     * @param x1 = point 1 - X-co
     * @param y1 = point 1 - Y-co
     * @param z1 = point 1 - Z-co
     * @param x2 = point 2 - X-co
     * @param y2 = point 2 - Y-co
     * @param z2 = point 2 - Z-co
     */
    private void drawLine(float x1, float y1, float z1, float x2, float y2, float z2)
    {
    	gl.glLoadIdentity();					// Reset The Current Modelview Matrix
		gl.glColor3f(0f, 0f, 0f);
		gl.glTranslatef(0.0f,0.0f,0.0f);
		gl.glBegin(GL.GL_LINES);
			gl.glVertex3d(x1, y1, 0.0f);
			gl.glVertex3d(x2, y2, 0.0f);
		gl.glEnd();
    }
    
    /**
     * Retrieve both objects for drawing a relation
     * @param objI		= object nr 1 already known
     * @param objIIid	= id of object nr 2, will be retrieved
     * @param x
     */
    public void drawRelation(ObjectInfo objI, int objIIid) {
    	ObjectInfo objII = null;
    	for(ObjectInfo objectinfo : model.getobjectsList()){
    		
			if(objectinfo.getId() == objIIid)
			{
				objII = objectinfo;				
				float[] points = computeRelationPoints(objI, objII);
				drawLine(points[0], points[1], points[2], points[3], points[4], points[5]);
				break;
			}
		}
    	
    }
    
    /**
     * Determines the translation from point (0,0,0) for placing 
     * an object on the scene
     * @param objectinfo = object that will be placed in the objectdiagram
     * @param size		 = number of all objects in the application
     * @param i			 = current object number in the list
     * @return
     */
    public void placeObjects(ObjectInfo objectinfo, int size, int i)
    {
		if(model.getAutoposition() == true)
		{	
			int angle = 360/8;//model.getobjectsList().size();//
			if(model.getobjectsList().size() > 8){angle = 360/size;}
			int hypotenuse = 10;			
			transX = (float) (Math.sin(Math.toRadians(angle * i)) * hypotenuse) * 1.8f;
			transY = (float) (Math.cos(Math.toRadians(angle * i)) * hypotenuse);
		}
		else
		{
			transX = objectinfo.getX();
			transY = objectinfo.getY();
			transZ = objectinfo.getZ();
		}		
        objectinfo.setX(transX);
        objectinfo.setY(transY);
        objectinfo.setZ(transZ);
    }
    
    /**
     * This method is responsible of drawing the correct relations
     * @param objectinfo = objectinfo, which relations that will drawn
     */
    public void handleRelations(ObjectInfo objectinfo){
        if(objectinfo.getRelations().size() != 0)
        {
        	if(relationType == "relations")
        	{
	        	for(int id  : objectinfo.getRelations())
	        	{
	        		drawRelation(objectinfo, id);
	        	}
        	}
        	else if (relationType == "initializations")
        	{
            	gl.glEnable(GL.GL_LINE_STIPPLE);
        		gl.glLineStipple(1, (short) 0x00FF); // dashed 
            	for(int id  : objectinfo.getInitialisations()){
            		drawRelation(objectinfo, id);
            	}
            	gl.glDisable(GL.GL_LINE_STIPPLE);
        	}
        }
    }
    
    /**
     * Handles text generation for all fields of an object
     * @param objectinfo
     */
    public void handleFields(ObjectInfo objectinfo)
    {
	    if(objectinfo.getFields().size() != 0)
	    {
	    	float pos = 0.6f;
	    	HashMap<Field, Object> map = (HashMap<Field, Object>) objectinfo.getFields().clone();
	    	for(Entry<Field, Object> entry : map.entrySet())
	    	{
	    		String cleanType  = cleanName(entry.getKey().typeName());
				String cleanField = cleanName(entry.getKey().toString());
				
				String fieldValue = ""; 										// value of a field
				if(entry.getValue() != null)fieldValue = entry.getValue().toString();
				
	    		createText(cleanType + " " + cleanField + " :" + fieldValue, transX - 1.0f , transY - pos, transZ);
	        	pos += 0.6f;
	    	}
	    }
    }
    
    /**
     * Determine the attach points on the two objects which have a 
     * relation whit each other. (For drawing a line) Points will be determined
     * using the position and size of the two objects.
     * @param objI	= Objectinfo number 1
     * @param objII = Objectinfo number 2
     * @return = collection of floating point with the two coordinates of the attach points
     */
    public float[] computeRelationPoints(ObjectInfo objI, ObjectInfo objII)
    {
    	float x1 = 0.0f, x2 = 0.0f, y1 = 0.0f, y2 = 0.0f, z1 = 0.0f, z2 = 0.0f;    	
    	float classTopHeight = objI.getTopHeight();
    	x1 = objI.getX();
		x2 = objII.getX();
    	if(objI.getX() < objII.getX()-(objII.getWidth()+2.0))
    	{
    		x1 = (objI.getX() + (objI.getWidth()/2));
    		x2 = (objII.getX() - (objII.getWidth()/2));
    		y1 = objI.getY() - (((objI.getHeight() + classTopHeight)/2)-1);// - yI;
    		y2 = objII.getY()- (((objII.getHeight() + classTopHeight)/2)-1);// - yII;
    	}
    	else if(objI.getX()-(objII.getWidth()+2.0) > objII.getX())
    	{
    		x1 = (objI.getX() - (objI.getWidth()/2));
    		x2 = (objII.getX() + (objII.getWidth()/2));
    		y1 = objI.getY() - (((objI.getHeight() + classTopHeight)/2)-1);// - yI;
    		y2 = objII.getY()- (((objII.getHeight() + classTopHeight)/2)-1);// - yII;
    	}
    	else if(objI.getY() < objII.getY())
    	{
    		y1 = objI.getY() + classTopHeight;
    		y2 = objII.getY() - objII.getHeight();// - yI;;
    	}
    	else if(objI.getY() > objII.getY())
    	{
    		y1 = objI.getY() - objI.getHeight();// - yI;
    		y2 = (objII.getY() + classTopHeight);
    	}
    	else
    	{
    		System.err.println("Objects are to close no line will be drawed");
    	}
    	float[] points = {x1, y1, z1, x2, y2, z2};
    	return points;
    }
    
    /**
     * Create text in the canvas
     * @param text  = text that will be rendered
     * @param x 	= X-co where the text will be displayed
     * @param y		= Y-co where the text will be displayed
     * @param z		= Z-co where the text will be displayed
     */
    public void createText(String text, float x, float y, float z)
    {   	
    	gl.glLoadIdentity();					// Reset The Current Modelview Matrix
    	gl.glColor3f(0f, 0f, 0f);
    	gl.glTranslatef(x,y,0.0f);
		gl.glRasterPos3f(-3.9f, 0.0f, 0.1f);//-1.9f
		
		glut.glutBitmapString(GLUT.BITMAP_HELVETICA_10, text);
		//glut.glutBitmapString(GLUT.BITMAP_8_BY_13, text);
		//glut.glutBitmapString(GLUT.STROKE_MONO_ROMAN, text);
	}
    
    /**
     * Return the string after the last "." (dot)
     * @param name = String that will split
     * @return
     */
    private String cleanName(String name)
    {
    	String[] fullName = name.toString().split("\\."); 
    	String cleanName = fullName[(fullName.length-1)];
		return cleanName;    	
    }
    
     /**
     * Resizes the screen.
     * @see javax.media.opengl.GLEventListener#reshape(javax.media.opengl.GLAutoDrawable,
     *      int, int, int, int)
     */
    public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
        final GL gl = drawable.getGL();
        gl.glViewport(0, 0, width, height);
    }

    /**
     * Changing devices is not supported.
     * @see javax.media.opengl.GLEventListener#displayChanged(javax.media.opengl.GLAutoDrawable,
     *      boolean, boolean)
     */
    public void displayChanged(GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged) {
        throw new UnsupportedOperationException("Changing display is not supported.");
    }

    /**
     * @param gl The GL context.
     * @param glu The GL unit.
     * @param distance The distance from the screen.
     */
    private void setCamera(GL gl, GLU glu, float distance) {
        // Change to projection matrix.
        gl.glMatrixMode(GL.GL_PROJECTION);
        gl.glLoadIdentity();

        // Perspective.
        float widthHeightRatio = (float) getWidth() / (float) getHeight();
        glu.gluPerspective(45, widthHeightRatio, 1, 1000);
        glu.gluLookAt(0, 0, distance, 0, 0, 0, 0, 1, 0);

        // Change back to model view matrix.
        gl.glMatrixMode(GL.GL_MODELVIEW);
        gl.glLoadIdentity();
    }

	public void initalise(Model model) {
		this.model = model;
	}

	public void mouseClicked(MouseEvent e) {
	}

	public void mouseEntered(MouseEvent e) {
	}

	public void mouseExited(MouseEvent e) {
	}

	/**
	 * Check if an object is selected when mouse is pressed
	 */
	public void mousePressed(MouseEvent e) {
		if(model.getAutoposition() == false)
		{
			isDragging = true;
		}
		
		float factor = this.getHeight()/floatingPoints; //pixels per floating point
		oldMouseX = (float) ((e.getX() - (this.getWidth()/2))/factor);
		oldMouseY = (float) (((this.getHeight()/2)- e.getY())/factor);
		for(ObjectInfo objectinfo : model.getobjectsList()){
			if(objectinfo.getX() > oldMouseX - objectinfo.getWidth()/2 && objectinfo.getX() < oldMouseX + objectinfo.getWidth()/2 && objectinfo.getY() > oldMouseY - 1.0f && objectinfo.getY() < oldMouseY)
			{
				for(ObjectInfo objectinfo2 : model.getobjectsList()){
					objectinfo2.setSelected(false);
				}
				objectinfo.setSelected(true);
				model.notifyObs();
				dragged = objectinfo.getId();
				break;
			}
		}	
	}

	/**
	 * Dragging is stopped
	 */
	public void mouseReleased(MouseEvent e) {
		isDragging = false;
		dragged = -1;
	}

	/**
	 * If an object is selected drag it
	 */
	public void mouseDragged(MouseEvent e) {
		float factor = this.getHeight()/floatingPoints; //pixels per floating point
		if(isDragging)
		{
			float newMouseX = (float) ((e.getX() - (this.getWidth()/2))/factor);
			float newMouseY = (float) (((this.getHeight()/2)- e.getY())/factor);
			float Xtrans = newMouseX - oldMouseX;
			float Ytrans = newMouseY - oldMouseY;
			for(ObjectInfo objectinfo : model.getobjectsList()){
				if(objectinfo.getId() == dragged)
				{
					objectinfo.setX(objectinfo.getX() + Xtrans);
					objectinfo.setY(objectinfo.getY() + Ytrans);
					break;
				}
			}
			oldMouseX = newMouseX;
			oldMouseY = newMouseY;
		}
	}

	@Override
	public void mouseMoved(MouseEvent e) {
		// TODO Auto-generated method stub
	}
	
	public void revalidateGUI()
	{
        /*
         * Revalidate canvas because JOGLCanvas is an AWT-component and all
         * other GUI-components are SWING components (this gives some problems). 
         * Quick fix for a bug that is available from java 6 update 19
         */
        Container parent = this.getParent();
        if (parent instanceof JComponent) {
            ((JComponent)parent).revalidate();
        }
        // ... and just in case, call validate() on the top-level window as well
        Window window = SwingUtilities.getWindowAncestor(this);
        if (window != null) {
            window.validate();
        }
	}
}
